'use strict';

const fs = require('fs');
const terser = require('terser');
const csso = require('csso');
const htmlstate = require('./htmlstate');
const path = require('path');

const fsp = fs.promises;

const htmlparser = new htmlstate();

function delayOutError (err, info, delay = 1280) {
  setTimeout(() => {
    let errText = '';

    if (typeof err === 'string') {
      errText = `\x1b[1;35m${info}\n\x1b[0m  \x1b[1;33m${err}\x1b[0m`;
    } else {
      errText = `\x1b[1;35m${info}\n\x1b[0m  ${err.message}`;
    }

    console.error(errText);
  }, delay);
}

/**
 * 要根据pages记录的页面去指定的目录中加载页面，并生成一些初始化的代码。
 */
let _page_dir = __dirname;

let wapp = function (options = {}) {

  if (!(this instanceof wapp) ) {
    return new wapp(options);
  }

  this.mydir = _page_dir;

  this.codeTempPath = this.mydir + '/temp';

  try {
    fs.accessSync(this.codeTempPath)
  } catch(err) {
    fs.mkdirSync(this.codeTempPath)
  }

  this.pageUrlPath = '/';

  this.defaultVersion = '2.3';

  this.version = '2.3';

  this.forceCompress = false;

  this.config = {
    manifest: '',
    test : false,
    debug: false,
    title : '',
    tabs : [],
    tabsPages : [],
    tabsPageIndex : {},
    pages : [],
    css : '',
    cssurl : '',
    jsurl : '',
    jsbottom: '',
    hooks : [],
    host : '',
    port : '',
    prepath: '',
    closePrompt: true,
    pagedir : '',
    components : [],
    extends: [],
    exts: [],
    iconPath: '/favicon.ico',
    asyncPage: false,
  };

  this.jch = {
    css : '',
    js  : '',
  };

  this.getCssCode = (cssfile, attach = false) => {
    let csscode = fs.readFileSync(
                    `${this.mydir}/v${this.version}/${cssfile}`,
                    {encoding: 'utf8'}
                  );

    if (attach) {
      this.jch.css += csscode;
    } else {
      this.jch.css = csscode;
    }
  };

  this.init = async () => {
    let wdir = `${this.mydir}/v${this.version}`;

    try {
      fs.accessSync(wdir);
    } catch (err) {
      console.error(err);
      wdir = `${this.mydir}/v${this.defaultVersion}`;
    }

    this.jch.css += fs.readFileSync(`${wdir}/w.css`, {encoding: 'utf8'});
    this.jch.js = fs.readFileSync(wdir + '/w.js', {encoding: 'utf8'});

    if (this.forceCompress || this.config.debug === false || (this.isbuild && this.config.buildCompress)) {
      let data = await terser.minify(this.jch.js+'\n');
      if (data.code === undefined) {
        console.error(data.error);
      } else {
        this.jch.js = data.code;
      }
    }

    let ds = this.pageUrlPath[this.pageUrlPath.length - 1] === '/' ? '' : '/';

    if (!this.isbuild) {
      this.iconlink = `<link href="${this.pageUrlPath}${ds}favicon.ico" rel="icon" type="image/x-icon">`;
    } else if (this.isbuild && this.config.iconPath) {
      this.iconlink = `<link rel="icon" href="${this.config.iconPath}" type="image/x-icon">`;
    }

    this.manifest = '';

    if (this.config.manifest) {
      if (typeof this.config.manifest === 'boolean') {
        this.manifest = `<link rel="manifest" href="${this.pageUrlPath}${ds}manifest.json">`;
      } else {
        this.manifest = `<link rel="manifest" href="${this.config.manifest}">`;
      }
      
    }

  };

  if (typeof options === 'object') {
    for (let k in options) {
      switch (k) {
        case 'debug':
        case 'closePrompt':
        case 'test':
          this.config[k] = options[k]; break;
        
        case 'pagedir':
          this.config.pagedir = options[k];
          let psplit = this.config.pagedir.split('/').filter(p => p.length > 0);
          if (psplit.length > 0) {
            this.pageUrlPath = `/${psplit[psplit.length - 1]}/`;
          }
          //this.config.prepath = this.pageUrlPath;
          break;

        default:;
      }
    }
  }

  this.pagesCode = '';
  this.tabsHTML = '';

  this.hooksText = '';
  this.components = '';
  this.extends = '';
  this.jstext = '';
  this.cssCode = '';
  this.csstext = '';
  this.jsbottom = '';
  this.isbuild = false;
  this.buildPrePath = '';
  this.iconlink = '';
  this.templates = '';

  this.compile = function () {
    //window.onunload = function () {return '退出？';};
    let closePromptText = `window.onbeforeunload = function (e) {return e.returnValue = '退出应用？';};`
      +`window.onpagehide = evt => {if (evt.persisted) {} return '退出?';};`
      +`window.onunload = function () {w.destroyAllPage();};`;

    if (this.config.closePrompt === false) {
      closePromptText = 'window.onunload = function () {w.destroyAllPage();};';
    }

    return `<!DOCTYPE html><html>
      <head>
        <title id="app-title">${this.config.title}</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,user-scalable=no">
        ${this.iconlink}
        ${this.manifest}
        ${this.csstext}
        <style>
          ${csso.minify(this.jch.css+'\n').css}
          ${csso.minify(this.cssCode+'\n').css}
        </style>
        <style>a{outline:none;text-decoration: none;}</style>
          ${this.jstext}
        <script>
          ${this.jch.js}
        </script>
      </head>
      <body style="overflow-x:hidden;overflow-wrap:break-word;">
        <div>${this.templates}</div>
        <script>
          'use strict';
          w.host = '${this.config.host}';
          window.__prepath__ = w.prepath = '${this.config.prepath}';
          w.homepage = '${this.config.pages[0]}';
          w.__title__ = '${this.config.title}';
          w.curTitle = '${this.config.title}';
          w.debug = ${this.config.debug ? 'true' : 'false'};

          w.tabs.list = ${JSON.stringify(this.config.tabs)};
          w.tabs.pages = ${JSON.stringify(this.config.tabsPages)};
          w.tabs.pageIndex = ${JSON.stringify(this.config.tabsPageIndex)};
          w.tabs.background = '${this.config.tabsBackground}';
          w.tabs.selectedBackground = '${this.config.tabsSelectedBackground}';
          
          window.alert = w.alert.bind(w);
          window.unalert = w.unalert.bind(w);
          window.alertError = w.alertError.bind(w);
          window.notify = w.notify.bind(w);
          window.notice = w.notice.bind(w);
          window.notifyError = w.notifyError.bind(w);
          window.prompt = w.prompt.bind(w);
          window.unprompt = w.unprompt.bind(w);
          window.notifyTop = w.notifyTop.bind(w);
          window.notifyLight = w.notifyLight.bind(w);
          window.notifyTopError = w.notifyTopError.bind(w);
          window.notifyOnly = w.notifyOnly.bind(w);
          window.unnotify = w.unnotify.bind(w);
          window.promptMiddle = w.promptMiddle.bind(w);
          window.promptTop = w.promptTop.bind(w);
          window.promptGlass = w.promptGlass.bind(w);
          window.promptTopGlass = w.promptTopGlass.bind(w);
          window.promptTopDark = w.promptTopDark.bind(w);
          window.promptMiddleGlass = w.promptMiddleGlass.bind(w);
          window.promptDark = w.promptDark.bind(w);
          window.promptMiddleDark = w.promptMiddleDark.bind(w);
          window.acover = w.acover.bind(w);
          window.coverShadow = w.coverShadow.bind(w);
          window.uncover = w.uncover.bind(w);
          window.uncoverShadow = w.uncoverShadow.bind(w);
          window.setCoverText = w.setCoverText.bind(w);
        </script>
        <script>'use strict';${this.libCode}</script>
        <script>'use strict';${this.extends}</script>
        <script>'use strict';${this.components}</script>
        <script>'use strict';${this.hooksText}</script>
        <script>'use strict';${this.pagesCode}</script>
        <script>
        'use strict';

        window.onload = async function () {
          let dms = [
            'pgcdom','coverdom','notifydom','alertdom','alertdom1', 'slidedom', 'alertcoverdom',
            'alertcoverdom1', 'tabsdom','tabsmenudom', 'historydom','slidexdom', 'promptdom', 'navibtndom', 'promptclosedom'
          ];

          for (let i=0; i<dms.length; i++) {
            w[ dms[i] ] = document.body.insertBefore(
              document.createElement('div'),
              document.body.firstChild
            );
          }

          ${this.config.asyncPage ? 'await new Promise(rv => {setTimeout(() => {rv();}, 10);});' : ''}

          w.initPage();
          initPages();

          if (w.tabs.list.length > 0) {
            w.tabsmenudom.className = 'w-tabbar-row-x';
            w.tabsmenudom.background = '${this.config.tabsBackground}';
            w.tabsmenudom.innerHTML = \`${this.tabsHTML}\`;
          }
        };
        
        function initPages () {
          for (let p in w.pages) {
            w.pages[p].view = function (data,options={}) {return w.view(p,data,options);};
            w.pages[p].render = function (htext) {return w.fmtHTML(p, htext);};
            w.pages[p].setScroll = function(scr) {
              if (scr < 0) { w.pages[p].__dom__.scrollTop += scr; }
              else { w.pages[p].__dom__.scrollTop = scr; }
            };
            w.pages[p].destroy = function () {w.destroyPage(w.pages[p]);};
            w.pages[p].query = function(qstr) {return w.pages[p].__dom__.querySelector(qstr);};
            w.pages[p].queryAll = function(qstr) {return w.pages[p].__dom__.querySelectorAll(qstr);};
            w.pages[p].setAttr = function (data) {w.setAttr(p,data);};
            if (!w.pages[p].data || typeof w.pages[p].data !== 'object') {
              w.pages[p].data = {};
            }
            w._make_page_bind(p);
            w._page_style_bind(p);
          }
        }

        window.onpageshow = async function() {
          await new Promise(rv => {setTimeout(() => {rv();}, 29);});
          if (w.init && typeof w.init === 'function') w.init();
          if (w.tabs.list.length > 0 && w.tabs.pageIndex[w.homepage] !== undefined && location.hash.length < 2)
          {
            w.switchTab(w.homepage);
          } else {
            w.listenHash();
          }
          
        };

        window.jump_page_forward = false;

        document.addEventListener('click', evt => {
          let t = evt.target;
          let ft = evt.currentTarget;
          if ((!t || !t.tagName || t.tagName !== 'A') && ft) {
            t = ft;
          }

          if (t.activeElement && t.activeElement.tagName === 'A') {
            let href = t.activeElement.href || '';
            if (href === 'javascript:history.back();' || href === 'javascript:history.go(-1);')
            {
              window.jump_page_forward = -1;
              return;
            }
          }
        
          let url = '';
          if (t && t.tagName && t.tagName === 'A') {
            url = t.href;
          } else if (t.documentURI) {
            url = t.documentURI;
          }

          if (url) {
            let cur_url = location.protocol + '/'+'/' + location.host + location.pathname;
            if (url.indexOf(cur_url) === 0 && url.indexOf('#') > 0) {
              window.jump_page_forward = true;
            }
          }
        });

        window.onhashchange = async function(e) {
          if (w.hashchange && typeof w.hashchange === 'function') {
            if (w.hashchange(e) === false) {
              return;
            }
          }

          await new Promise((rv, rj) => { setTimeout( () => { rv(); }, 15); });

          let parsehash = (h) => {
            let ind = h.indexOf('#');
            if (ind < 0) return '#';
            return h.substring(ind);
          };
          
          let hashstr = parsehash(e.oldURL);
          let new_hashstr = parsehash(e.newURL);

          if (w.historyList.length > 95) {
            for (let i = 0; i < 50; i++) {
              w.historyList.shift();
            }
            w.historyList.unshift('#');
          }

          let ind = w.historyList.indexOf(hashstr);

          let op = '';
          if (w.historyLength < history.length || window.jump_page_forward === true) {
            op = 'forward';
            window.jump_page_forward = false;
          } else {
            
            if (ind > 1) {
              if (w.historyList[ind-1] === new_hashstr) {
                op = 'back';
              } else {
                op = 'forward';
              }
            } else {
              op = 'back';
            }

            if (window.jump_page_forward === -1) {
              window.jump_page_forward = false;
              op = 'back';
            }

          }

          w.historyList.push(new_hashstr);
          w.historyLength = history.length;

          w.listenHash(op);
        };

        window.onscroll = function (){w.events.scroll();};
        window.onresize = function (){w.events.resize();};
        ${closePromptText}
        </script>
        ${this.jsbottom}
      </body>
    </html>`;
  }
  
};

wapp.prototype.loadConfig = function (cfgfile, isbuild = false) {

  let makeTabMenu = function (m, width, urlpath) {
    let mtext = `<div class="w-tabbar-row-cell" id="w-t-mmmm-${m.page}" onclick="w.switchTab('${m.page}');" name="${m.page}" style="width:${width}%;">`;
    
    if (m.icon && typeof m.icon === 'string' && m.icon.length > 0) {
      mtext += `<img src="${urlpath}${m.icon}" style="max-height: 1.35rem;height:auto;width:auto;" name="${m.page}">`;
    }

    if (m.name && typeof m.name === 'string') {
      mtext += `<div name="${m.page}" style="font-size:81%;line-height:101%;padding-bottom:0.125rem;">${m.name}</div>`;
    }
    
    mtext += `</div>`;

    return mtext;
  };

  let prepath = this.pageUrlPath;
  prepath = prepath.replace(/\/{2,}/ig, '/');

  try {
    let cfg = require(cfgfile);
    if (cfg.buildPrePath && isbuild) {
      this.buildPrePath = cfg.buildPrePath;
      prepath = cfg.buildPrePath;
      if (prepath.length > 0 && prepath[prepath.length - 1] !== '/') {
        prepath += '/';
      }
    }

    for (let k in cfg) {
      switch (k) {
        case 'title':
          this.config.title = cfg.title;
          break;

        case 'iconPath':
          this.config.iconPath = cfg[k];
          break;
        
        case 'pages':
          if (cfg[k] instanceof Array) {
            this.config.pages = cfg.pages;
          }
          break;

        case 'tabs':
          
          if (cfg.tabs.list && cfg.tabs.list instanceof Array && cfg.tabs.list.length > 0) {
            let width = 100 / cfg.tabs.list.length;
            this.config.tabs = cfg.tabs.list;
            let pageIndex = 0;
            for (let a of this.config.tabs) {
              this.tabsHTML += makeTabMenu(a, width, prepath);
              this.config.tabsPages.push(a.page);
              this.config.tabsPageIndex[a.page] = pageIndex;
              pageIndex += 1;
            }
            
          }
          if (cfg.tabs.background) {
            this.config.tabsBackground = cfg.tabs.background;
          }
          if (cfg.tabs.selectedBackground) {
            this.config.tabsSelectedBackground = cfg.tabs.selectedBackground;
          }
          break;
        
        case 'host':
          if (this.config.host === '') {
            this.config[k] = cfg[k];
          }
          break;
        
        case 'animation':
          if (cfg[k] === true) {
            this.getCssCode('w-animation.css', true);
          }
          break;

        case 'pageAnimation':
          if (cfg[k] === true) {
            this.getCssCode('w-page-animation.css', true);
          } else if (cfg[k] === 'dropdown') {
            this.getCssCode('w-page-animation-dropdown.css', true);
          }
          break;

        case 'radius':
          if (cfg[k]) {
            let rname = '';
            switch (cfg[k]) {
              case true:
                rname = '';break;
              case 'small':
              case 'large':
                rname = '-' + cfg[k];break;
            }
            this.getCssCode(`w-radius${rname}.css`, true);
          }
          break;
        
        case 'version':
          this.version = cfg[k];
          break;

        default:
          this.config[k] = cfg[k];
      }
    }
    if (cfg.testHost && this.config.test) {
      this.config.host = cfg.testHost;
    }
  } catch (err) {
    console.error(err.message);
  }

};

wapp.prototype.parseErrorStack = function (stack, linestart, filename) {
  let arr = stack.split('\n');

  if (arr.length <= 0) return stack;

  let first = arr[0].split(':');

  if (first.length <= 1 || isNaN(parseInt(first[1].trim())) ) return stack;

  let n = parseInt(first[1].trim()) - linestart - 1;

  let narr = [ `${filename}:${n}` ];

  for (let i = 1; i < arr.length; i++) {
    narr.push(arr[i]);
    if (arr[i].indexOf('SyntaxError') >= 0 || (/.*Error/ig).test(arr[i])) break;
  }

  return narr.join('\n');

}

//如果Function检测出现错误，这说明存在语法问题。此时为了更精确的得到问题代码，利用require的方式来打包代码进行检测。
wapp.prototype.requireCheckCode = function (filename, ctext, options = {}) {
  let requireFile = `${this.codeTempPath}/__require_module__.js`;

  let initRequireEnv = () => {
    return `let require = async function (name) {
      try {
        if (w.__ext__[name]) return w.__ext__[name];
        
        let loop = w.__require_loop__;
    
        for (let i = 0; i < loop; i++) {
          await new Promise((rv) => {
            setTimeout(() => { rv(); }, 2);
          });
    
          if (w.__ext__[name]) return w.__ext__[name];
        }
    
        return function () {};
      } catch (err) {
        console.error(err.message);
      }
    };`;
  }

  let globalBind = () => {
    return `for (let k in window) {
      global[k] = window[k];
    }
    
    for (let k in window.globalThis) global[k] = window(k);
    `;
  };

  let flag_tag = `;/* ${Math.random()} */;`;

  let pkgCode = `'use strict';\nconst {window,document,w,Component} = require('../webenv.js');`
              + `\n;(async (exports) => {\n${initRequireEnv()}\n`
              + `\n${flag_tag}\n${ctext}\n})(w.${options.exportsKey || 'ext'});`;

  let flagind = pkgCode.indexOf(flag_tag);
  let linestart = 0;
  for (let i = 0; i < flagind; i++) {
    if (pkgCode[i] === '\n') linestart += 1;
  }

  fs.writeFileSync(requireFile, pkgCode, {encoding: 'utf8'});

  try {
    let r = require(requireFile);
    console.log(filename, 'ok');
  } catch (err) {
    if (err.stack) delayOutError(this.parseErrorStack(err.stack, linestart, filename), '');
    else delayOutError(err, filename);
  }

  //检测后必须删除对应的缓存。
  delete require.cache[requireFile];
  delete require.cache[path.resolve(requireFile+'/../../webenv.js')]

}

wapp.prototype.checkCode = function (filename, ctext, options = {async: true}) {
  let asy = 'async';
  !options.async && (asy = '');

  //return this.requireCheckCode(filename, ctext, options);
  
  try {
    let testcode = `'use strict';
                let w = {ext:{},hooks:[],events:{},config:{},__ext__:{},};let alert = () => {};
                let notify = () => {}; let confirm = () => {}; function Component () {};
                let window = {}; let document={};let exports = {};let require = function (pkg) {};
                \n;(${asy} (exports) => {${ctext}})(w.ext);`;

    let t = Function(testcode);
    console.log(filename, 'ok');
  } catch (err) {
    //console.error('--CHECK-CODE:', filename, err);
    //delayOutError(err, filename);
    this.requireCheckCode(filename, ctext, options);
  }
};

wapp.prototype.fmtPageHTML = function (ht, pagename) {
  ht = ht.replace(/<!DOCTYPE html>/ig, '');
  ht = ht.replace('<html>', '');
  ht = ht.replace('</html>', '');
  ht = ht.replace('<body>', '');
  ht = ht.replace('</body>', '');
  ht = ht.replace('<head>', '');
  ht = ht.replace('</head>', '');
  ht = ht.replace('<meta>', '');
  ht = ht.replace(/<script>/g, '');
  ht = ht.replace(/<\/script>/g, '');

  //匹配onclick ondblclick 等事件属性，前面必须有空格，否则会引起错误，比如button class=""
  //因为onsubmit="return false;"等类似的形式，会导致处理错误，这需要引入语法分析，所以暂时注释了。
  //页面中，需要明确带上self.表示是页面对应的操作函数。
  ht = ht.replace(/ on[^(=|"|'|;)]+="[^"]+"/g, m => {
    let sp = m.split('=');
    let fstr = sp[1].substring(1, sp[1].length-1);
    let retFalse = '';

    if (fstr.indexOf('self.') != 0 && fstr.indexOf(' ') < 0 && fstr.indexOf('w.') != 0) {
      fstr = `w.pages.${pagename}.${fstr}(this);`;
      if (sp[0] === ' onsubmit') {
        retFalse = 'return false;';
      }
      return `${sp[0]}="${fstr}${retFalse}"`;
    }

    return m;
  });

  ht = ht.replace(/ on[^=]+=[^(\s|>|"|')]+/g, m => {
    let sp = m.split('=');
    let fstr = sp[1];

    if (fstr.indexOf('self.') != 0 && fstr.indexOf('w.') != 0) {
      fstr = `w.pages.${pagename}.${fstr}(this);`;
      if (sp[0] === ' onsubmit') {
        return `${sp[0]}="${fstr}return false;"`;
      }
      return `${sp[0]}=${fstr}`;
    }

    return m;
  });

  let p = 0;

  let replaceSelf = (from, qs='"') => {
    p = ht.indexOf(from);
    while (p >= 0) {
      ht = ht.replace(from, `=${qs}w.pages.${pagename}.`);
      p = ht.indexOf(from, p + 10);
    }
  }

  replaceSelf('="self.');
  replaceSelf("='self.", "'");
  replaceSelf('=self.', '');

  return ht;
};

wapp.prototype.replaceCssUrl = function (codetext) {

  let replace_src = (url) => {
    if ((/^http[s]?:\/\//).test(url)) {
      return url;
    }

    if (this.isbuild) {
      return `${this.buildPrePath}/${url}`.replace(/\/{2,}/ig, '/');
    }

    return `${this.pageUrlPath}/${url}`.replace(/\/{2,}/ig, '/');
  };

  let match_replace = m => {
    let url = m.substring(5, m.length-1);
    return `url(${replace_src(url)})`;
  };

  codetext = codetext.replace(/url\([^\(]+\)/g, match_replace);

  return codetext;
};

wapp.prototype.replaceSrc = function (codetext, is_comps = false, comp_name = '') {

  let replace_src = (url, plist, offset, text) => {
    if ((/^http[s]?:\/\//).test(url)) {
      return url;
    }

    let turl = url.trim();
    if (turl.indexOf('${') === 0) {
      return url;
    }

    //不做替换处理。
    if (turl[0] === '!') {
      return turl.substring(1);
    }

    if (this.isbuild) {
      return `${this.buildPrePath}/${url}`.replace(/\/{2,}/ig, '/');
    }

    return `${this.pageUrlPath}/${url}`.replace(/\/{2,}/ig, '/');

  };

  let match_replace = m => {
    let arr = m.split(' src=');
    
    let q = arr[1][0];
    let startind = 1;
    let endind = arr[1].length - 1;

    /* if (q !== '"' && q !== "'") {
      q = '';
      startind = 0;
      endind++;
    } */

    let orgsrc = arr[1].substring(startind, endind);

    //针对组件
    if (is_comps) {
      orgsrc = orgsrc.replace('./static', '/component/' + comp_name);
    }

    let final_src = `${arr[0]} src=${q}${replace_src(orgsrc)}${q}`;

    return final_src;
  };

  //audio embed iframe img input source track video
  codetext = codetext.replace(
    /<(audio|embed|iframe|img|input|source|track|video)[^>]* src="[^"]+"/ig, 
    match_replace);

  codetext = codetext.replace(
    /<(audio|embed|iframe|img|input|source|track|video)[^>]* src='[^']+'/ig, 
    match_replace);

  /* codetext = codetext.replace(
      /<(audio|embed|iframe|img|input|source|track|video)[^>]* src=[^\s]+ /g, 
      match_replace); */

  return codetext;

};

wapp.prototype.loadPage = function (pagefile, htmlfile, cssfile, pagename) {
  let htext = '';

  try {
    fs.accessSync(htmlfile);
    htext = fs.readFileSync(htmlfile, {encoding: 'utf8'});
    htext = this.fmtPageHTML(htext, pagename);
  } catch (err) {
    delayOutError(err, '--LOAD-PAGE--');
  }

  try {
    this.pagesCode += '\n';
    let ctext = fs.readFileSync(pagefile, {encoding: 'utf8'});

    if (this.config.asyncPage) {
      ctext = this.replaceRequire(ctext);
    }

    this.checkCode(pagefile, ctext, {async: this.config.asyncPage});
    this.pagesCode += `;(${this.config.asyncPage ? 'async ' : ''}`
      + `function(exports){${ctext};exports.${pagename}.orgHTML=\`${htext}\`;})(w.pages);`;
  } catch (err) {
    delayOutError(err, '--LOAD-PAGE--');
    delayOutError('有错误或不存在，请检查', pagefile);
  }

  try {
    fs.accessSync(cssfile);
    let csstext = fs.readFileSync(cssfile, {encoding: 'utf8'});
    csstext = this.replaceCssUrl(csstext);
    //console.log(csstext);
    this.cssCode += csstext;
  } catch (err) {
    //console.error(err.message);
  }

};

wapp.prototype.libCode = '';

/**
 * 
 * lib目录中的文件以!开头不加载。
 * @param {string} libdir 
 */
wapp.prototype.loadLib = function (libdir) {
  try {
    let kn = '';
    let names = [];
    let flist = fs.readdirSync(libdir, {withFileTypes: true});
    for (let f of flist) {
      if (!f.isFile()) continue;
      
      if (f.name[0] === '!' || f.name.substring(f.name.length - 3) !== '.js')
          continue;
      
      names.push(f.name);
    }

    let orgdata;

    for (let n of names) {
      orgdata = fs.readFileSync(`${libdir}/${n}`, 'utf8') + '\n';
      this.checkCode(`${libdir}/${n}`, orgdata);
      this.libCode += `;(function(exports){${orgdata}})(window);`;
    }

  } catch (err) {
    delayOutError(err, '--LIB-CODE--');
  }
};

// replace <- to await require
wapp.prototype.replaceRequire = function (ctext) {
  return ctext.replace(/\=[\s]{0,}<-[\s]{0,}\([\s]{0,}\'/ig, '= await require(\'')
              .replace(/\=[\s]{0,}<-[\s]{0,}\([\s]{0,}\"/ig, '= await require("')
              .replace(/\=[\s]{0,}<-[\s]{0,}\([\s]{0,}\`/ig, '= await require(`')
              .replace(/\=[\s]{0,}require[\s]{0,}\([\s]{0,}\'/ig, '= await require(\'')
              .replace(/\=[\s]{0,}require[\s]{0,}\([\s]{0,}\"/ig, '= await require("')
              .replace(/\=[\s]{0,}require[\s]{0,}\([\s]{0,}\`/ig, '= await require(`');
}

wapp.prototype.loadExt = async function (cdir) {
  try {
    let data = '';
    let orgdata = '';

    if (this.config.extends === '*'
      || (this.config.extends.length > 0 && this.config.extends[0] === '*') )
    {
      let flist = fs.readdirSync(`${cdir}`, {withFileTypes: true})
      this.config.extends = []

      for (let f of flist) {
        if (!f.isFile() || f.name.substring(f.name.length - 3) !== '.js')
          continue;

        if (f.name[0] === '!') continue;

        this.config.extends.push(f.name.substring(0, f.name.length - 3));
      }
    }
    
    let names = this.config.extends;

    for (let i=0; i < names.length; i++) {

      try {
        orgdata = fs.readFileSync(`${cdir}/${names[i]}.js`, 'utf8') + '\n';
        
        orgdata = this.replaceRequire(orgdata);

        this.checkCode(`${cdir}/${names[i]}.js`, orgdata);

        this.extends += `;(async function(exports){${orgdata}})(w.ext);`;

      } catch (err) {
        console.error(err.message);
      }
    }

    //进行src替换处理
    this.extends = this.replaceSrc(this.extends);

    if (this.forceCompress || this.config.debug === false || (this.isbuild && this.config.buildCompress)){
      data = await terser.minify(this.extends);
      if (data.error) {
        console.error(data.error);
      } else {
        this.extends = data.code;
      }
    }

  } catch (err) {
    console.error(err.message);
  }
};

wapp.prototype.buildCompsStatic = async function (cdir, names, stdir) {
  
  let d;
  let flist;
  let src = '';

  for (let a of names) {
    d = `${stdir}/${a}`;

    await fsp.access(d).catch(err => {
      return fsp.mkdir(d);
    });

    src = `${cdir}/${a}/static`;

    try {
      flist = await fsp.readdir(src, {withFileTypes: true});
    } catch (err) {
      continue;
    }

    for (let f of flist) {
      if (!f.isFile()) continue;

      await fsp.copyFile(`${src}/${f.name}`, `${d}/${f.name}`)
                .catch(err => {
                    console.error(err);
                });
    }
    
  }

  try {
    let dlist = await fsp.readdir(stdir, {withFileTypes: true});
    let oldlist;

    for (let r of dlist) {
      if (!r.isDirectory()) continue;

      //删除不再启用的组件。
      if (names.indexOf(r.name) < 0) {
        await fsp.rm(`${stdir}/${r.name}`,{recursive:true}).catch(err => {
          console.error(err);
        });
        continue;
      }

      oldlist = await fsp.readdir(`${stdir}/${r.name}`, {withFileTypes: true});

      for (let o of oldlist) {
        await fsp.access(`${cdir}/${r.name}/static/${o.name}`)
          .catch(async err => {
            return await fsp.unlink(`${stdir}/${r.name}/${o.name}`);
          })
          .catch(err => {
            console.error(err);
          });
      }

    }
  } catch (err) {
    console.error('--CLEAR-COMPONETS-STATIC--', err);
  }

};

wapp.prototype.readImportCss = function (cssfiles, cdir) {

  let css_dir = cdir + '/../@css';

  let code = '';
  let temp = '';

  for (let f of cssfiles) {
    try {
      temp = fs.readFileSync(`${css_dir}/${f.file}`, {encoding: 'utf8'});
      code += csso.minify(temp + '\n').css;
    } catch (err) {
      console.error(err)
    }
  }

  return code;
};

wapp.prototype.replaceImportCss = function (text, cdir) {

  text = text.replace(/\/\*(.|[\r\n])*?\*\//mg, '');

  let style_start = text.indexOf('<style>');
  let endind = text.indexOf('</style>');

  if (style_start < 0) return text;

  let i = 0;
  let cssfiles = [];

  let parseFile = (t) => {
    let f = t.substring(('@import').length).trim();
    if (f.indexOf('url(') >= 0) {
      f = f.substring(4, f.length - 1);
    }
    return f.substring(1, f.length - 1);
  }

  let iend = 0;
  let start = style_start + 7;

  while (start < endind) {
    i = text.indexOf('@import', start);

    if (i < 0) break;

    iend = text.indexOf(';', i);

    cssfiles.push({
      pos: {
        start: i,
        end: iend
      },
      file: parseFile(text.substring(i, iend))
    });

    start = iend + 1;
  }

  if (cssfiles.length === 0) {
    let compress_css = csso.minify(text.substring(style_start+7, endind)+'\n').css;
    return text.substring(0, style_start + 7) 
          + this.replaceCssUrl(compress_css)
          + text.substring(endind);
  }

  let csscode = this.readImportCss(cssfiles, cdir);

  let replace_start = cssfiles[0].pos.start;

  let replace_end = cssfiles[ cssfiles.length - 1 ].pos.end;

  let replace_text = text.substring(0, replace_start) 
                        + this.replaceCssUrl(csscode)
                        + text.substring(replace_end+1);
  
  endind = replace_text.indexOf('</style>');

  let compress_css = csso.minify(replace_text.substring(style_start+7, endind)+'\n').css;
  return replace_text.substring(0, style_start + 7) 
              + compress_css
              + replace_text.substring(endind);
}

/**
 * 一个组件是一个目录，其中包括和目录同名的.js文件、explain.json文件、.html文件，若html文件不存在则表示不存在template。
 * explain.json文件描述组件的类名和组件名称，以及相关其他描述，其属性如下：
 *  - name 组件名称
 *  - className 类名称
 *  - detail 相关的详细描述
 * @param {string} cdir 
 */

wapp.prototype.loadComps = async function (cdir, appdir) {
  try {
    fs.accessSync(cdir + '/@css');
  } catch (err) {
    fs.mkdirSync(cdir + '/@css');
  }

  try {
    let data = '';
    let orgdata = '';

    if (this.config.components === '*'
      || (this.config.components.length > 0 && this.config.components[0] === '*') )
    {
      let flist = fs.readdirSync(`${cdir}`, {withFileTypes: true})

      this.config.components = []

      for (let f of flist) {
        if (!f.isDirectory()) continue;

        if (f.name[0] === '!') continue;

        this.config.components.push(f.name.substring(0, f.name.length - 3));
      }
    }
    
    let names = this.config.components;
    let cex;
    let opts = '';
    let tempdata = '';

    for (let i=0; i < names.length; i++) {

      //检测组件是否存在相关文件。
      try {
        fs.accessSync(`${cdir}/${names[i]}/explain.json`);
        fs.accessSync(`${cdir}/${names[i]}/${names[i]}.js`);
        cex = require(`${cdir}/${names[i]}/explain.json`);
      } catch (err) {
        delayOutError(err, '检测组件文件是否存在', 1234);
      }

      tempdata = '';
      try {
        fs.accessSync(`${cdir}/${names[i]}/template.html`);
        tempdata = fs.readFileSync(`${cdir}/${names[i]}/template.html`, {encoding: 'utf8'});
        if (!htmlparser.parse(tempdata)) {
          delayOutError(htmlparser.lastErrorMsg, `语法检测：${names[i]}/template.html`);
        } else {
          
          tempdata = tempdata.replace(/<!--(.|[\r\n])*?-->/mg, '');
          tempdata = this.replaceSrc(tempdata, true, names[i]);
          //检测是否有@import导入css文件，根据@import导入@css目录的css。
          tempdata = this.replaceImportCss(tempdata, `${cdir}/${names[i]}`);

          //使用div包装模板。
          this.templates += `<div data-id="${cex.name}">${tempdata}</div>`;
        }
      } catch (err) {
        
      }

      try {
        orgdata = fs.readFileSync(`${cdir}/${names[i]}/${names[i]}.js`, 'utf8') + '\n';
        
        this.checkCode(`${cdir}/${names[i]}.js`, orgdata);

        opts = '';
        if (cex.options && cex.options.extends) {
          opts = `,{extends: '${cex.options.extends}'}`;
        }

        orgdata = this.replaceSrc(orgdata, true, names[i]);

        this.components += `;(()=>{${orgdata};customElements.define('${cex.name}', ${cex.className}${opts});})();`;
      } catch (err) {
        console.error(err.message);
      }
    }

    if (this.isbuild) {
      let comp_dir = appdir + '/_static/_components';
      await fsp.access(comp_dir).catch(err => {
        return fsp.mkdir(comp_dir);
      });

      this.buildCompsStatic(cdir, names, comp_dir);
    }

    //进行src替换处理
    //this.components = this.replaceSrc(this.components, true);

    if (this.forceCompress || this.config.debug === false || (this.isbuild && this.config.buildCompress)){
      data = await terser.minify(this.components);
      if (data.error) {
        console.error(data.error);
      } else {
        this.components = data.code;
      }
    }

  } catch (err) {
    console.error(err.message);
  }
};

wapp.prototype.makeApp = async function (appdir = '', isbuild = false) {

  let pdir = appdir || this.config.pagedir;

  let pathname = '';
  for (let i = pdir.length - 1; i >= 0; i--) {
    if (pdir[i] === '/' || pdir[i] === '\\') {
      break;
    }
    pathname = `${pdir[i]}${pathname}`;
  }

  try {
    let cfgfile = `${pdir}/config.json`;
    fs.accessSync(cfgfile);
    this.loadConfig(cfgfile, isbuild);
  } catch (err) {
    console.error(err);
    throw err;
  }

  await this.init();

  this.config.prepath = this.pageUrlPath;

  if (isbuild) {
    this.config.prepath = this.buildPrePath;
    this.config.debug = false;
  }

  if (this.config.prepath.length > 0) {
    let lend = this.config.prepath.length - 1;
    if (this.config.prepath[lend] === '/')
      this.config.prepath = this.config.prepath.substring(0, lend);
  }

  try {
    fs.accessSync(`${pdir}/app.css`);
    let appcss = fs.readFileSync(`${pdir}/app.css`, {encoding: 'utf8'}) + '\n';
    appcss = this.replaceCssUrl(appcss);
    this.cssCode += appcss;
  } catch(err){}

  try {
    fs.accessSync(`${pdir}/_extends`, fs.constants.F_OK);
    await this.loadExt(`${pdir}/_extends`);
  } catch (err){
    console.error(err);
  }

  try {
    fs.accessSync(`${pdir}/_components`, fs.constants.F_OK);
    await this.loadComps(`${pdir}/_components`, pdir);
  } catch (err){
    console.error(err);
  }

  if (this.templates.length > 0) {
    this.templates = this.replaceCssUrl(this.templates);
  }

  try {
    await this.loadLib(`${pdir}/_lib`);
  } catch (err) {
    console.error(err);
  }

  let hookData = '';
  for (let h of this.config.hooks) {
    try {
      hookData = fs.readFileSync(`${pdir}/_hooks/${h}.js`,'utf8') + '\n';

      hookData = this.replaceRequire(hookData);

      this.checkCode(`${pdir}/_hooks/${h}.js`, hookData, {exportsKey: 'hookFunc'});
      
      this.hooksText += `;w.hooks.push('${h}');(async function(exports){${hookData}})(w.hookFunc);`;
    } catch (err) {
      console.error(err.message);
    }
  }

  if (this.hooksText.length > 0) {
    if (this.config.debug === false || this.forceCompress || (this.isbuild && this.config.buildCompress)) {
        hookData = await terser.minify(this.hooksText);
        if (hookData.error) {
          console.error(hookData.error);
        } else {
          this.hooksText = hookData.code;
        }
    }
  }

  if (this.libCode.length > 0) {
    if (this.forceCompress || (this.isbuild && this.config.buildCompress)) {
      let libCodeCompress = await terser.minify(this.libCode);
      if (libCodeCompress.error) {
        console.error(libCodeCompress.error);
      } else {
        this.libCode = libCodeCompress.code;
      }
    }
  }

  let makeRealSrc = (url) => {
    if ((/^http[s]?:\/\//i).test(url)) {
      return url;
    }

    let prepath = pathname;

    if (isbuild) {
      prepath = this.config.buildPrePath;
    }

    if (prepath.length > 0 && prepath[prepath.length - 1] !== '/' && url[0] !== '/') {
      prepath += '/';
    }

    if (prepath.length > 0 && prepath[0] !== '/') {
      prepath = `/${prepath}`;
    }

    return `${prepath}${url}`;
  };

  if (this.config.jsurl) {
    if (typeof this.config.jsurl === 'string') {
      this.config.jsurl = [this.config.jsurl];
    }

    for (let j of this.config.jsurl) {
      this.jstext += `<script src="${makeRealSrc(j)}"></script>`;
    }

  }

  if (this.config.jsbottom) {
    if (typeof this.config.jsbottom === 'string') {
      this.config.jsbottom = [ this.config.jsbottom ];
    }

    for (let jb of this.config.jsbottom) {
      this.jsbottom += `<script src="${makeRealSrc(jb)}"></script>`;
    }
  }

  //load css file for link
  if (this.config.cssurl) {
    if (typeof this.config.cssurl === 'string') {
      this.config.cssurl = [this.config.cssurl];
    }

    for (let  c of this.config.cssurl) {
      this.csstext += `<link href="${makeRealSrc(c)}" rel="stylesheet">`;
    }
  }

  for (let page of this.config.pages) {
    this.loadPage(
      `${pdir}/${page}/${page}.js`,
      `${pdir}/${page}/${page}.html`,
      `${pdir}/${page}/${page}.css`,
      page
    );
  }

  //进行src替换处理
  this.pagesCode = this.replaceSrc(this.pagesCode);

  //压缩页面
  if (this.forceCompress || this.config.debug === false || (this.isbuild && this.config.buildCompress)) {
    let data = await terser.minify(this.pagesCode + '\n');
    if (data.code === undefined) {
      console.error(data.error);
    } else {
      this.pagesCode = data.code;
    }
  }

  return this.compile();

};

/**
 * @param {string} appname app的目录名字
 * @param {string} bdir 构建app的目标目录
 */
wapp.prototype.build = async function (appdir, appname = '') {
  
  this.isbuild = true;

  let data = await this.makeApp(appdir, true);

  try {
    fs.writeFileSync(`${appdir}/${appname || 'index'}.html`, data, {encoding:'utf8'});
  } catch (err) {
    err && console.error(err);
  }
};

wapp.prototype.newPage = function (name, pagedir) {
  
  let html = `exports.${name} = new function () {

  this.onload = function (c) {
  
  };

  this.onshow = function (c) {

  };

  this.onhide = function () {

  };

  this.onunload = function () {

  };

  this.onbottom = function () {

  };

  this.onscroll = function (scrollTop, clientHeight, scrollHeight) {

  };

  this.ontop = function () {

  };

  this.onresize = function () {

  };

};`;

  let pdir = `${pagedir}/${name}/`;
  let pagefile = `${pagedir}/${name}/${name}.js`;
  let htmlfile = `${pagedir}/${name}/${name}.html`;
  let cssfile = `${pagedir}/${name}/${name}.css`;

  try {
    fs.accessSync(pdir);
  } catch (err) {
    console.log('创建目录：'+pdir);
    fs.mkdirSync(pdir);
  }

  try {
    fs.accessSync(pagefile);
  } catch (err) {
    console.log('创建文件：'+pagefile);
    fs.writeFileSync(pagefile, html, {encoding:'utf8'});
  }

  try {
    fs.accessSync(htmlfile);
  } catch (err) {
    console.log('创建文件：'+htmlfile);
    fs.writeFileSync(htmlfile, `<div>This is ${name} page.</div>`, {encoding:'utf8'});
  }
  
  try {
    fs.accessSync(cssfile);
  } catch (err) {
    console.log('创建文件：'+cssfile);
    fs.writeFileSync(cssfile, '/**CSS文件，建议在样式名前面统一加上页面名称**/', {encoding:'utf8'});
  }
  
};

function checkCompsName (cname) {
  cname = cname.trim();
  if (cname.length < 3) return false;

  if (cname.indexOf('-') < 0) return false;

  if (!(/^[a-z]/i).test(cname)) return false;

  return true;
}

function fmtCompsClassName (cname) {
  let namearr = []

  let end = cname.length - 1;

  for (let i = 0; i < cname.length; i++) {
    if (i === 0) namearr.push(cname[i].toUpperCase());
    else if (cname[i] === '-' && i < end && (/[a-z]/i).test(cname[i+1])) {
      namearr.push(cname[i+1].toUpperCase());
      i += 1;
    } else {
      namearr.push(cname[i])
    }
  }

  return namearr.join('');
};

function renderExplainJSON (cname) {
  let className = fmtCompsClassName(cname);
  return `{
    "name" : "${cname}",
    "version": "1.0.0",
    "className" : "${className}",
    "detail" : "...",
    "doc" : "readme.md"
}`;
}

function renderCompsClass (cname) {
  let className = fmtCompsClassName(cname);

  return `'use strict';\n
class ${className} extends Component {

  constructor () {
    super(); //必须写在最开始。

    //通过this.attrs访问所有属性。this.attributes是浏览器原始提供的属性对象。
    //this.attrs是为了方便而做的映射。

    //this.shadow可以访问shadow DOM，注意这是shadowRoot。
    //this.host用于访问组件对应的DOM，this.host指向this.shadow.host。

  }

  //在render之前执行，此时已经创建好shadow DOM。
  init () {

  }

  //返回字符串或DOM节点。
  render () {
    // 也可以返回字符串 return '${cname}组件';
    return this.plate();
  }

  //渲染完成后执行
  afterRender () {
    
  }

  onload () {

  }

  //从DOM树中移除时触发。
  onremove () {

  }

  onattrchange (name, oldValue, newValue) {

  }

  //被移动到新文档时触发。
  onadopted () {

  }

  static get observedAttributes() {
    //如果你要监控某些属性的变化，你需要在onattrchange中处理。
    //要在属性变化时触发onattrchange函数，你需要在此函数中返回对应的属性。
    //return ['class', 'name']; 
  }


}`;

}

wapp.prototype.newComps = function (cname, cdir) {

  if (!checkCompsName(cname)) {
    console.error(`${cname} 不符合要求，至少3字符长度，字母开头，包含 - 。`);
    return false;
  }

  let compsdir = `${cdir}/_components/${cname}`;

  try {
    fs.accessSync(compsdir)
    console.error(`${cname} 组件已存在。`);
    return false;
  } catch (err) {
    
  }

  fs.mkdirSync(compsdir);

  fs.mkdirSync(compsdir + '/static');

  fs.writeFileSync(`${compsdir}/explain.json`, renderExplainJSON(cname), {encoding: 'utf8'});

  fs.writeFileSync(`${compsdir}/readme.md`, `# ${cname}\n`, {encoding: 'utf8'});

  fs.writeFileSync(`${compsdir}/${cname}.js`, renderCompsClass(cname), {encoding: 'utf8'});

  fs.writeFileSync(`${compsdir}/template.html`, 
    `<template>\n<style>\n/* 目前浏览器part属性(参考MDN ::part)不支持组件嵌套，如果组件需要嵌套使用请注意样式问题。 */\n</style>\n\n<div>${cname}组件</div>\n</template>\n`, 
    {encoding: 'utf8'});

  return true;

};

//new project
wapp.prototype.newProject = function (project_dir) {
  try {
    fs.accessSync(project_dir);
    console.log(project_dir, '已经存在，如果需要重新创建，可以删除此目录或重命名。');
    return false;
  } catch (err) {

  }

  try {
    fs.mkdirSync(project_dir);
  } catch (err) {
    console.error(err);
    return false;
  }

  let new_project_dir = __dirname + '/newproject';

  let cpfiles = ['app.css', 'config.json', 'favicon.ico', 'manifest.json'];

  for (let i = 0; i < cpfiles.length; i++) {
    try {
      fs.copyFileSync(`${new_project_dir}/${cpfiles[i]}`, `${project_dir}/${cpfiles[i]}`);
    } catch (err) {
      console.error(err.message);
    }
  }

  let loopcp = [
    '_components', '_extends', '_hooks', '_static', 'home', 'user', 'test',
    '_static/css', '_static/icon', '_static/images','_static/_components',
     'list', '_lib', '_components/u-card'
  ];

  for (let i = 0; i < loopcp.length; i++) {
    try {
      fs.mkdirSync(`${project_dir}/${loopcp[i]}`);
    } catch (err) {
      console.error(err.message);
    }
  }

  let copyFile = (dname, dest) => {
    let flist = fs.readdirSync(dname, {withFileTypes:true});
    for (let i = 0; i < flist.length; i++) {
      if (!flist[i].isFile()) {
        continue;
      }

      try {
        fs.copyFileSync(`${dname}/${flist[i].name}`, `${dest}/${flist[i].name}`);
      } catch (err) {
        console.error(err.message);
      }
    }
  };

  for (let i=0; i < loopcp.length; i++) {
    try {
      fs.accessSync(`${new_project_dir}/${loopcp[i]}`, fs.constants.F_OK);
    } catch (err) {
      console.error(err.message);
      continue;
    }

    copyFile(`${new_project_dir}/${loopcp[i]}`, `${project_dir}/${loopcp[i]}`);
  }

};

module.exports = wapp;
